package com.pic.mgt.haishin.executer;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.FilenameUtils;
import org.springframework.dao.DataAccessException;

import com.pic.mgt.IgxStatus;
import com.pic.mgt.dao.BasicBusinessTargetActionInfoBean;
import com.pic.mgt.dao.haishin.HaishinBusinessAsgmtActionInfoBean;
import com.pic.mgt.dao.haishin.IAppointedHaishinBusinessAsgmtActionDao;
import com.pic.mgt.dao.haishin.IHaishinBusinessMgtDao;
import com.pic.mgt.dao.haishin.IHaishinBusinessTargetActionDao;
import com.pic.mgt.executer.SimpleConfirmationFileMaker;
import com.pic.mgt.file.io.FileBuilder;
import com.pic.mgt.log.IgxLog;
import com.pic.mgt.manager.terminal.BasicTerminalInfoBean;
import com.pic.mgt.manager.terminal.TerminalManager;
import com.pic.mgt.monitor.parser.ConfirmtaionFileForTxFileParser;
import com.pic.mgt.unit.Business;
import com.pic.mgt.unit.Redoable;
import com.pic.mgt.utils.FilePathUtils;

public abstract class AsgmtFileProcess implements AsgmtAction {
	public static enum AsgmtBuildResult {
		AsgmtBuildComplete,
		IneffectiveTerminal}
	
	// 指示檔檔名的型態字元
	public static String typeCharForAsgmtFilename = "G";
	// 指示檔檔名的結尾字串
	public static String suffixStringForAsgmtFilename = "_0000000000";
	// 指示檔內容分隔字串
	private final String asgmtContentSeparation = ";";
	// 指示檔內容開頭字串
	private final String asgmtPrefixContent = "D";
	
	
	private TerminalManager haishinTerminalManager;
	private AsgmtListener asgmtListener;
	private FileBuilder fileBuilder;
	private String haishinOutbox;
	private String haishinOutboxPublic;	
	private int businessTypeCharPosition;
	private IHaishinBusinessMgtDao haishinBusinessMgtDao;
	private IAppointedHaishinBusinessAsgmtActionDao appointedHaishinBusinessAsgmtActionDao;
	private IHaishinBusinessTargetActionDao haishinBusinessTargetActionDao;
	
	private boolean asgmtBuilding = false;
	protected Business business;
	protected String targetFilename;
	protected long targetFilesize;
	protected Date executionTime;
	
	// 此二變數是互斥的，不會同時都不等於 null.
	private String redoAsginTerminal = null;
	private Map<String, Object> lastExecutedAsgmtTxFileMap = null;
	
	// 和 redoAsginTerminal 配搭使用的
	private Map<String, Object> redoAsginTerminalFileMap;
	
	private boolean isRedoMode;

	@Override
	public synchronized void build(Business business, String targetFilename,
								   Long targetFilesize, Date executionTime,
								   List<String> appointedTerminalList)
								  throws BuildAsgmtFileProcessException {
		this.business = business;
		this.targetFilename = targetFilename;
		this.targetFilesize = targetFilesize;
		this.executionTime = executionTime;
		
		// 先 Check 此業務是否為 redo 的
		this.isRedoMode = Redoable.class.isInstance(this.business);
		
		if (this.isRedoMode) {
			// 如果是屬於 redo 的 business 則要做前置處理
			doRedoHaishinBusinessPreprocess();
		}
		
		// 讓子類別做初始化 (如果有必要的話)
		try {
			initial();
		} catch (AsgmtFileProcessInitialException e1) {
			throw new BuildAsgmtFileProcessException(
					"Can't build ASGMT file for target file '" +
					targetFilename + "'", e1);
		}
		
		try {
			this.asgmtBuilding = true;
			
			// 取得指示檔各部份內容
			
			// 取得指示檔內容的開頭字串
			String asgmtCNTPrefix = this.asgmtPrefixContent +
									this.asgmtContentSeparation;			
			// 取得指示檔內容的指定路徑
			String asgmtCNTSpecificPath = getSpecificPathContentInAsgmtFile() +
										  this.asgmtContentSeparation;
			// 取得指示檔內容的指定檔名
			String asgmtSpecificFilename = getSpecificFilenameContentInAsgmtFile();
			
			// 產生指示檔至每個端末資料夾
			for(String terminal : appointedTerminalList) {
				// 取得指示檔的檔名
				String asgmtFilename = getAsgmtFilename(terminal);
				File asgmtFile = FilePathUtils.multiPathConcat(
									haishinOutbox,
									terminal,
									asgmtFilename
									);
				
				/*
				 *  如果現在正在進行的業務是 redo 的,則呼叫方法去檢查,
				 *  在 redo 模式下,該交易檔是否要被處理.如果沒有需要處理則忽略
				 */
				if (this.isRedoMode) {
					if (isFilteredTxFileInRedoMode(terminal, asgmtFilename)) {
						continue;
					}
				}
				
				
				// 驗證是否為合法端末				
				BasicTerminalInfoBean basicTerminalInfoBean = isEffectiveTerminal(terminal);
				if (null == basicTerminalInfoBean) {
					// 指示檔建立結果通知
					asgmtFileBuildNotify(terminal, asgmtFile,
							 AsgmtBuildResult.IneffectiveTerminal);
					continue;
				}
				
				// 取得指示檔內容的執行時間
				String asgmtCNTexecutionTime = 
					getSpecificExecutionTimeContentInAsgmtFile(
							this.executionTime, basicTerminalInfoBean) +
					this.asgmtContentSeparation;
				
				
				// 建立指示檔內容
				fileBuilder.append(asgmtCNTPrefix);
				fileBuilder.append(asgmtCNTexecutionTime);
				fileBuilder.append(asgmtCNTSpecificPath);
				fileBuilder.append(asgmtSpecificFilename);
				try {
					// throws IOException
					fileBuilder.buildFile(asgmtFile);
					
					// 指示檔建立結果通知
					boolean isContinue = 
						asgmtFileBuildNotify(terminal, asgmtFile,
											 AsgmtBuildResult.AsgmtBuildComplete);
					if (!isContinue) {
						throw new BuildAsgmtFileProcessException(
								"Building ASGMT file '" + asgmtFile.getName() + "' for transaction file '" +
								asgmtSpecificFilename + "' has been interrupted.");
					}
				} catch (IOException e) {
					throw new BuildAsgmtFileProcessException(
							"Building ASGMT file '" + asgmtFile.getName() + "' for transaction file '" +
							asgmtSpecificFilename + "' failed.", e);
				}
			}
			
			// 更新 business mgt table 狀態
			try {
				this.haishinBusinessMgtDao.updateStatusByUnitName(
						IgxStatus.BUSINESS_EXECUTING_FINISH, business.getUnitName());
			} catch (DataAccessException e) {
				throw new BuildAsgmtFileProcessException(
						"Building ASGMT files for transaction file '" +
						asgmtSpecificFilename + "' failed in final updating database status (table: " +
						this.haishinBusinessMgtDao.getTableName() + ").", e);
			}
			
		} finally {
			this.asgmtBuilding = false;
			
			this.redoAsginTerminal = null;
			this.redoAsginTerminalFileMap = null;
			this.lastExecutedAsgmtTxFileMap = null;
		}
	}
	
	private boolean isFilteredTxFileInRedoMode(String terminal,
											   String asgmtFilename) {
		if (null != this.redoAsginTerminal) {
			/* 表示此 redo 的 business 指定只配給某一個端末.
			 * 只要是符合這個端末和指定配給檔案的交易檔都要配. 
			 */
			
			if (this.redoAsginTerminal.equals(terminal)) {
				if (null != this.redoAsginTerminalFileMap) {
					// 表示有指定要配給的檔案，有在 Map 中的檔案才能配
					return
						!this.redoAsginTerminalFileMap.containsKey(asgmtFilename);
				}
				else {
					// 表示沒有指定要配給的檔案，就是只要是該端末的檔案都要配
					return false;
				}
			}
			else {
				// 不符合端末 ID ，不配。
				return true;
			}
		}
		else {
			/*
			 * 表示根據這個業務上次執行的況狀，續繼未完成的配信動作。
			 * lastExecutedAsgmtTxFileMap 內為上次執行已完成配給的交易檔清單，
			 * 所以只要在這清單內的交易檔都不用再做一次了。
			 */
			
			return this.lastExecutedAsgmtTxFileMap.containsKey(asgmtFilename);
		}
	}
	
	// 指示檔建立結果通知
	private boolean asgmtFileBuildNotify(String terminal, File asgmtFile,
									  AsgmtBuildResult asgmtBuildResult) {
		if (this.asgmtListener != null) {
			return
				this.asgmtListener.asgmtProcessNotify(
									this.business,
									terminal,
									asgmtFile,
									asgmtBuildResult);
		}
		
		return true;
	}
	
	/**
	 * 判斷該端末ID是否為有效的端末。
	 * @param terminal
	 * @return 若為有效端末則會回傳該端末的 BasicTerminalInfoBean；
	 * 	　　　    若為無效端末則回傳Null。
	 */
	private BasicTerminalInfoBean isEffectiveTerminal(String terminal) {
		if (this.haishinTerminalManager.isEffectiveTerminalId(terminal)) {
			BasicTerminalInfoBean basicTerminalInfoBean = 
					this.haishinTerminalManager.getBasicTerminalInfoBean(terminal);
			if (null != basicTerminalInfoBean) {
				return basicTerminalInfoBean;
			}
			else {
				return null;
			}
		}
		else {
			return null;
		}
	}
	
	private String getAsgmtFilename(String terminal) {
		// 產生一個名稱,是與目標檔檔名相同,但在配信型態的字元換成 'G'
		char[] targetFilenameCharArray = this.targetFilename.toCharArray();
		targetFilenameCharArray[this.businessTypeCharPosition - 1] =
										AsgmtFileProcess.typeCharForAsgmtFilename.charAt(0);
		
		// 產生指示配信檔名
		String asgmtFilename = terminal + "_" +
						  	   new String(targetFilenameCharArray) +
						  	   AsgmtFileProcess.suffixStringForAsgmtFilename;
		return asgmtFilename;
	}
	
	private String getSpecificPathContentInAsgmtFile() {
		String tempPath = FilenameUtils.getName(this.haishinOutbox);
		
		File specificPath = new File(File.separator + tempPath,
									 this.haishinOutboxPublic);
		
		return specificPath.getAbsolutePath() + File.separator;
	}
	
	private String getSpecificFilenameContentInAsgmtFile() throws BuildAsgmtFileProcessException {
		// 從資料庫中去找出指示配信要取的檔案名稱
		try {
			BasicBusinessTargetActionInfoBean basicBusinessTargetActionInfoBean = 
				this.haishinBusinessTargetActionDao.findBasicBusinessTargetActionInfoByUnitName(business.getTargetFilename());
			if (null == basicBusinessTargetActionInfoBean) {
				throw new BuildAsgmtFileProcessException(
						"Building ASGMT file failed. Can't find business '" +
						this.targetFilename +
						"' in database (table: " +
						this.haishinBusinessTargetActionDao.getTableName() +
						").");
			}
			else {
				return basicBusinessTargetActionInfoBean.getFilename();
			}
		} catch (DataAccessException e) {
			throw new BuildAsgmtFileProcessException(
					"Building ASGMT file failed. Query business '" +
					this.targetFilename +
					"' from database (table: " +
					this.haishinBusinessTargetActionDao.getTableName() +
					") occured an error.", e);
		}
	}
	
	private void doRedoHaishinBusinessPreprocess() throws BuildAsgmtFileProcessException {
		// 從資料庫取得該 redo 的 business 是否有指定要配給的端末
		String redoAsginTerminal = null;
		String redoAsginTerminalFile = null;
		try {
			// DataAccessException
			String[] redoAsginInfo = this.haishinBusinessMgtDao.
											findRedoAsginTerminalAndRedoAsginTerminalFileByUnitName(this.business.getUnitName());
			redoAsginTerminal = redoAsginInfo[0];
			redoAsginTerminalFile = redoAsginInfo[1];
		} catch (DataAccessException e) {
			throw new BuildAsgmtFileProcessException(
					"Redoing haishin business '" + this.business.getUnitName() +
					"' execution occured a database operation error in preprocess step.", e);
		}
		
		if (null == redoAsginTerminal) {
			/*
			 *  沒有指定要重配的端末,表示此 redo 的 business 要做恢復的動作.
			 *  即是根據業務上次配信的狀態,接著將還沒有完成的配信動作(即解tar)做完. 
			 */
			
			// ********************** Log **********************
			IgxLog.messageLog.info(this,
							"Recovering haishin business '{}' from last execution.",
							this.business.getUnitName());
			// ********************** Log **********************
			
			this.lastExecutedAsgmtTxFileMap = new HashMap<String, Object>();
			
			// 取出上次已經配送的 target file 清單 (按 creating time 由大到小)
			List<HaishinBusinessAsgmtActionInfoBean> lastHaishinExecutedAsgmtActionList;
			try {
				lastHaishinExecutedAsgmtActionList = 
							this.appointedHaishinBusinessAsgmtActionDao.
								findHaishinBasicBusinessAsgmtActionInfoListByUnitNameWithOrderByCreatingTimeDESC(
										this.business.getUnitName());
			} catch (DataAccessException e) {
				throw new BuildAsgmtFileProcessException(
						"Recovering haishin business '" + this.business.getUnitName() + 
						"' occured a database operation error.", e);
			}
			if (null == lastHaishinExecutedAsgmtActionList || lastHaishinExecutedAsgmtActionList.isEmpty()) {
				// 表示上回該業務沒有任何已經配成功的交易檔(即指示檔)
			}
			else {
				/* 取得第一筆出來(因為第一筆是上回配信業務在進行時中斷掉最後配的交易檔),
				 * 然後去判斷該筆是否上次沒配完,沒配完的話要重配給該筆。要重配的情況為:
				 * (1) 交易檔案存在，且交易檔的"暫存"確認檔也存在
				 * (2) 交易檔案存在，且交易檔的確認檔和確認"暫存"檔都不存在，且沒有上次最後交易時間
				 * 	　 (這種表示上次只做到交易檔產生出來為止而已，"暫存"的確認檔也都還沒產生出來)
				 * 符合2種情況之一，都要重配。
				 */
				HaishinBusinessAsgmtActionInfoBean lastHaishinExecutedAsgmtActionInfoBean = 
															lastHaishinExecutedAsgmtActionList.get(0);
				// 先假設該筆為上次已經執行過，並且是正確的執行(也就是不需要重配)
				this.lastExecutedAsgmtTxFileMap.put(lastHaishinExecutedAsgmtActionInfoBean.getAsgmtFilename(),
													null);
				if (lastHaishinExecutedAsgmtActionInfoBean.getResult().
						equals(IgxStatus.HAISHIN_ASGMT_ACTION_INEFFECTIVE_TERMINAL)) {
					// 該交易檔是屬於無效的端末,不用再配,加入已執行 Map 清單中  (上面已經加入過)
				}
				else {
					// 得到交易檔案
					File transactionFile = FilePathUtils.multiPathConcat(
														this.haishinOutbox,
														lastHaishinExecutedAsgmtActionInfoBean.getTerminalId(),
														lastHaishinExecutedAsgmtActionInfoBean.getAsgmtFilename());
					// 得到交易檔的確認檔
					File confirmationFileForTxFile = 
								ConfirmtaionFileForTxFileParser.getConfirmationFile(transactionFile);
					// 得到交易檔的確認檔的暫存檔 (確認檔暫存檔)
					File confirmationTmpFileForTxFile = SimpleConfirmationFileMaker.getTmepConfirmationFile(confirmationFileForTxFile);
					
					if (transactionFile.exists()) {
						// 表示交易檔存在
						
						if (confirmationTmpFileForTxFile.exists()) {
							// 表示確認暫存檔存在 (表示上回動作未完成，只做到產生暫存檔，必須重配)
							
							this.lastExecutedAsgmtTxFileMap.remove(lastHaishinExecutedAsgmtActionInfoBean.getAsgmtFilename());
						}
						else {
							// 表示確認暫存檔不存在
							
							if (!confirmationFileForTxFile.exists() &&
								null == lastHaishinExecutedAsgmtActionInfoBean.getLastTransactionTime()) {
								// 表示沒有確認檔也沒有上次最後交易時間 (表示上回動作未完成，只做到產生交易檔而已，必須重配)
								
								this.lastExecutedAsgmtTxFileMap.remove(lastHaishinExecutedAsgmtActionInfoBean.getAsgmtFilename());
							}
						}
					}
				}
				
				/*
				 * 將這些上回已經重配好的交易檔放入已執行 Map 清單中，除了第一筆不要放之外。
				 * 因為第一筆已在上面做另外的判斷和處理.
				 */
				for (int i=1;i<lastHaishinExecutedAsgmtActionList.size();i++) {
					this.lastExecutedAsgmtTxFileMap.put(
								lastHaishinExecutedAsgmtActionList.get(i).getAsgmtFilename(),
								null);
				}
			}
		}
		else {
			// 有指定配給某"一個"端末
			
			// 指定配給的端末
			this.redoAsginTerminal = redoAsginTerminal;
			
			/*
			 *  指定配給端末的哪些檔案 (主要是做系統恢覆時會用到,不是給 user 使用)。
			 *  如果有的話，將它放到 redoAsginTerminalFileMap 裡。
			 */
			if (null != redoAsginTerminalFile) {
				this.redoAsginTerminalFileMap = new HashMap<String, Object>();
				
				String[] redoAsginTerminalFileArray = redoAsginTerminalFile.split(" ");
				for (String asginFile : redoAsginTerminalFileArray) {
					this.redoAsginTerminalFileMap.put(asginFile, null);
				}
			}
			
			// ********************** Log **********************
			IgxLog.messageLog.info(this,
							"Redoing haishin business '{}' execution for terminal '{}'.",
							this.business.getUnitName(), this.redoAsginTerminal);
			// ********************** Log **********************
		}
	}
	
	/**
	 * 每次做建立確認檔時,初始化方法都會先被呼叫一次
	 */
	abstract void initial() throws AsgmtFileProcessInitialException;
	
	abstract String getSpecificExecutionTimeContentInAsgmtFile(Date initialExecutionTime, BasicTerminalInfoBean basicTerminalInfoBean);
	
	public void removeAsgmtListener() {
		if (!this.asgmtBuilding) {
			this.asgmtListener = null;
		}
	}
	
	public void setAsgmtListener(AsgmtListener asgmtListener) {
		if (!this.asgmtBuilding) {
			this.asgmtListener = asgmtListener;
		}
	}
	
	// --- getter and setter

	public void setHaishinOutbox(String haishinOutbox) {
		this.haishinOutbox = haishinOutbox;
	}

	public void setHaishinOutboxPublic(String haishinOutboxPublic) {
		this.haishinOutboxPublic = haishinOutboxPublic;
	}

	public AsgmtListener getAsgmtListener() {
		return asgmtListener;
	}

	public void setFileBuilder(FileBuilder fileBuilder) {
		this.fileBuilder = fileBuilder;
	}

	public void setHaishinTerminalManager(TerminalManager haishinTerminalManager) {
		this.haishinTerminalManager = haishinTerminalManager;
	}

	public void setBusinessTypeCharPosition(int businessTypeCharPosition) {
		this.businessTypeCharPosition = businessTypeCharPosition;
	}

	public void setHaishinBusinessMgtDao(
			IHaishinBusinessMgtDao haishinBusinessMgtDao) {
		this.haishinBusinessMgtDao = haishinBusinessMgtDao;
	}

	public void setAppointedHaishinBusinessAsgmtActionDao(
			IAppointedHaishinBusinessAsgmtActionDao appointedHaishinBusinessAsgmtActionDao) {
		this.appointedHaishinBusinessAsgmtActionDao = appointedHaishinBusinessAsgmtActionDao;
	}

	public void setHaishinBusinessTargetActionDao(
			IHaishinBusinessTargetActionDao haishinBusinessTargetActionDao) {
		this.haishinBusinessTargetActionDao = haishinBusinessTargetActionDao;
	}
}
